unit CPCoordSystem;

interface

uses Types, Graphics, CPBase, CPView, CPPlotCanvas, Contnrs;

type
  TCPCoordSystem = class;

  { Grid layer with a specified  color and cell size }
  TCPGridLayer = class
  private
    { Parent coordinate system }
    FCoordSystem: TCPCoordSystem;
    { Most left and top grid point in coordinate units }
    FTopLeft: TCPPoint;
    { Indices of most left and top grid lines }
    FTopLeftOffset: TPoint;
    { Indices of most right and bottom grid lines }
    FBottomRightOffset: TPoint;
    { Horizontal and vertical size of one grid cell in coordinate units }
    FGridCellSize: TCPPoint;
    { Numbers of horizontal and vertical grid lines }
    FLineCount: TPoint;
    { Color of grid lines }
    FColor: TColor;
    { Index of grid layer. This value indicates the order in the list of
      grid layers. The top most grid layer has an index of 0. }
    FIndex: Integer;
    { Returns Y-position of horizontal grid line by its index in canvas units }
    function GetHorizGridLine(Index: Integer): TCPFloat;
    { Returns X-position of horizontal grid line by its index in canvas units }
    function GetVertGridLine(Index: Integer): TCPFloat;
    { Draws a single axis mark
      @param PlotCanvas Canvas to draw on
      @param Position Position of axis mark in canvas units.
      @param AxisMarkSpan Width and height of a single axis mark.
      @param XAxis If true, Coord refers to the X-axis,
        otherwise it refers to the Y-axis. }
    procedure DrawAxisMark(PlotCanvas: TCPPlotCanvas;
      Position: TCPPoint; AxisMarkSpan: TCPPoint);
    { Draws a single axis label
      @param PlotCanvas Canvas to draw on
      @param Position Position of axis mark in canvas units.
      @param Coordinate to be written as axis label.
      @param XAxis If true, Coord refers to the X-axis,
        otherwise it refers to the Y-axis. }
    procedure DrawAxisLabel(PlotCanvas: TCPPlotCanvas;
      Position: TCPPoint; Padding: TCPFloat; Coord: TCPFloat; XAxis: Boolean);
    { Draws axis marks and textural descriptions for both axes }
    procedure DrawAxisTextMarks;
  public
    { Creates a new grid layer.
      @param CoordSystem Parent coordinate system
      @param Index of grid layer
      @param GridCellSize Horizontal and vertical cell size of grid layer
        in coordinate system units
      @param Color Color of new grid }
    constructor Create(CoordSystem: TCPCoordSystem; Index: Integer;
      GridCellSize: TCPVector; Color: TColor);
    { Draws grid level }
    procedure Draw;
  end;

  TCPCoordSystem = class
  private
    { Referenced view of plotter control }
    FView: TCPView;
    { Color palette }
    FPlotStyle: PCPPlotStyle;
    { List with grid layers }
    FGridLayers: TObjectList;
    { Cell size of main grid in canvas units }
    FGridCellSize: TCPPoint;
    { Number of visible grid layers }
    FGridLayerCount: Integer;
    { Coordinate origin in canvas units }
    FOrigin: TCPPoint;
    { If enabled, then only the axes are drawn. }
    FFastPreview: Boolean;
    { Calculates cell size of main grid }
    function GetMainGridSize(ViewScale: TCPFloat): TCPFloat;
    { Attempts to subdivide a grid with the specfied GridSize by the
      specified subdivision factor, so that it is not less than
      MinGridSize. If the subdivision is possible, then true is returend
      and the grid size is modified, otherwise false is returned and the
      grid size remains unchanged. }
    function TrySubdivide(MinGridSize: TCPFloat;
      var GridSize: TCPFloat; Factor: TCPFloat) : Boolean;
  public
    { Creates a new coordinate system.
      @param View View region that is used for painting.
      @param PlotStyle Style options used for painting. }
    constructor Create(View: TCPView; PlotStyle: PCPPlotStyle);
    destructor Destroy; override;
    { Recalclulates grid cell size and rebuild grid layers }
    procedure Rebuild;
    { Draw entire coordinate grid, not including the X and Y axes }
    procedure DrawGrid;
    { Draws X- and Y-axis with text and marks }
    procedure DrawAxes;
    { In fast preview mode the coordinate system is drawn without grid lines }
    property FastPreview: Boolean read FFastPreview write FFastPreview;
    { Sets or gets the view }
    property View: TCPView read FView write FView;
  end;

implementation

uses Math, SysUtils;

{ TCPCoordSystem }

constructor TCPCoordSystem.Create(View: TCPView; PlotStyle: PCPPlotStyle);
begin
  FView := View;
  FPlotStyle := PlotStyle;
  FGridLayers := TObjectList.Create;
  FGridLayerCount := 4;
  FFastPreview := false;
end;

destructor TCPCoordSystem.Destroy;
begin
  FGridLayers.Free;
  inherited;
end;

procedure TCPCoordSystem.DrawGrid;
var
  a: Integer;
begin
  if (FFastPreview) then // Don't draw grid in fast preview
    exit;
  FView.PlotCanvas.PenWidth := FPlotStyle.GridLineWidth;
  for a := FGridLayers.Count - 1 downto 0 do
    TCPGridLayer(FGridLayers[a]).Draw;
end;

function TCPCoordSystem.TrySubdivide(MinGridSize: TCPFloat;
  var GridSize: TCPFloat; Factor: TCPFloat) : Boolean;
var
  NewGridSize: TCPFloat;
begin
  NewGridSize := GridSize / Factor;
  Result := NewGridSize > MinGridSize;
  if Result then
    GridSize := NewGridSize;
end;


function TCPCoordSystem.GetMainGridSize(ViewScale: TCPFloat): TCPFloat;
const
  GridSubdivide = 10;
var
  ScaleExp: TCPFloat;
  MinGridSize: TCPFloat;
  ThresholdRatio: TCPFloat;
  //Transition: TCPFloat;

begin
  // Ratio of minimum cell size to canvas width:
  ThresholdRatio := FView.PlotCanvas.PtToUnits(FPlotStyle.GridMinCellSize) /
    FView.PlotCanvas.Bounds.Width;
  // Calculate minimum grid cell size:
  MinGridSize := ViewScale * ThresholdRatio;
  // Find power of GridSubdivide value nearest to OptGridSize:
  ScaleExp := LogN(GridSubdivide, MinGridSize); // Scale exponent
  { Transition is a value between 0 and 1 indicating
    how close the plotter scale is to a finer(=1) or sparser(=0)
    grid resolution: }
  //Transition := Ceil(ScaleExp) - ScaleExp;
  { Calculate power of GridSubdivide value that is closest to OptGridSize }
  Result := Power(GridSubdivide, Ceil(ScaleExp));
  { Try to further subdivide grid to get closer to the optimal grid size.
    Therefore try different integer subdivide factors: }
  if not TrySubdivide(MinGridSize, Result, 5) then
    TrySubdivide(MinGridSize, Result, 2);
end;

procedure TCPCoordSystem.Rebuild;
var
  a: Integer;
  Opacity, Factor: Double;
  Color: TColor;
  ViewScale: TCPFloat;
begin
  FGridLayers.Clear;
  Factor := 1;
  Opacity := 1;
  ViewScale := FView.ViewScale;
  FOrigin := FView.ToCanvas(CPToVector(0, 0));
  // Get grid cell size of main grid:
  FGridCellSize := CPToVector(
    GetMainGridSize(ViewScale), GetMainGridSize(ViewScale / FView.YAxisScale));
  for a := 0 to FGridLayerCount - 1 do
  begin
    Color := CPMergeColor(FPlotStyle.Colors.Background,
      ColorToRGB(FPlotStyle.Colors.Grid), Opacity * FPlotStyle.GridOpacity);
    FGridLayers.Add(TCPGridLayer.Create(
      self, a, CPMul(FGridCellSize, Factor), Color));
    Factor := Factor * 0.5;
    Opacity := Opacity * 0.5;
  end;
end;

procedure TCPCoordSystem.DrawAxes;
var
  PlotCanvas: TCPPlotCanvas;
  Origin: TCPPoint;
begin
  PlotCanvas := FView.PlotCanvas;
  PlotCanvas.PenColor := FPlotStyle.Colors.Axis;
  Origin := FView.ToCanvas(CPToVector(0, 0));
  PlotCanvas.HorizLine (Origin.Y);
  PlotCanvas.VertLine(Origin.X);
  // Draw axis text marks:
  if (FGridLayers.Count > 0) then
    TCPGridLayer(FGridLayers[0]).DrawAxisTextMarks;
end;

{ TCPGridLayer }

constructor TCPGridLayer.Create(CoordSystem: TCPCoordSystem; Index: Integer;
  GridCellSize: TCPVector; Color: TColor);
var
  B: TCPRect;
  View: TCPView;
begin
  FCoordSystem := CoordSystem;
  View := FCoordSystem.FView;
  FIndex := Index;
  FGridCellSize := GridCellSize;
  FColor := Color;
  B := View.ViewRect;
  FTopLeftOffset := Point(
    Ceil(B.Left / GridCellSize.X),
    Ceil(B.Top / GridCellSize.Y));
  // Get coordinates of upper left horizontal and vertical grid line:
  FTopLeft := CPToVector(
    FTopLeftOffset.X * GridCellSize.X,
    FTopLeftOffset.Y * GridCellSize.Y);
  FBottomRightOffset := Point(
    Ceil(B.Right / GridCellSize.X),
    Ceil(B.Bottom / GridCellSize.Y));
  FLineCount.X := FBottomRightOffset.X - FTopLeftOffset.X;
  FLineCount.Y := FBottomRightOffset.Y - FTopLeftOffset.Y;
end;

procedure TCPGridLayer.Draw;
var
  a: Integer;
  PlotCanvas: TCPPlotCanvas;
begin
  PlotCanvas := FCoordSystem.FView.PlotCanvas;
  PlotCanvas.PenColor := FColor;
  for a := 0 to FLineCount.X - 1 do
    if (FIndex = 0) or (Odd(a + FTopLeftOffset.X)) then
      PlotCanvas.VertLine(GetVertGridLine(a));
  for a := 0 to FLineCount.Y - 1 do
    if (FIndex = 0) or (Odd(a + FTopLeftOffset.Y)) then
      PlotCanvas.HorizLine(GetHorizGridLine(a));
end;

procedure TCPGridLayer.DrawAxisLabel(PlotCanvas: TCPPlotCanvas;
  Position: TCPPoint; Padding: TCPFloat; Coord: TCPFloat; XAxis: Boolean);
var
  Text: string;
  TextExtent: TCPFloat;
  Bounds: TCPBounds;
begin
  Position.X := Position.X + PlotCanvas.SmoothDistance(Padding);
  Position.Y := Position.Y + PlotCanvas.SmoothDistance(Padding);
  if Abs(Coord) < 1E-7 then
    Coord := 0;
  if (Abs(Coord) >= 1E6) then
    Text := FloatToStrF(Coord, ffGeneral, 14, 5)
  else
    Text := FloatToStrF(Coord, ffGeneral, 14, 10);
  Bounds := PlotCanvas.Bounds;
  if (XAxis) then
  begin
    TextExtent := PlotCanvas.TextHeight(Text) + Padding;
    Position.Y := Min(Position.Y, PlotCanvas.Bounds.Top +
      PlotCanvas.Bounds.Height - TextExtent);
  end else
  begin
    TextExtent := PlotCanvas.TextWidth(Text) + Padding;
    Position.X := Min(Position.X, PlotCanvas.Bounds.Left +
      PlotCanvas.Bounds.Width - TextExtent);
  end;
  PlotCanvas.PenColor := FCoordSystem.FPlotStyle.Colors.AxisText;
  PlotCanvas.TextOut(Position.X, Position.Y, Text);
end;

procedure TCPGridLayer.DrawAxisMark(PlotCanvas: TCPPlotCanvas;
      Position: TCPPoint; AxisMarkSpan: TCPPoint);
begin
  PlotCanvas.PenColor := FCoordSystem.FPlotStyle.Colors.AxisMark;
  if (AxisMarkSpan.X > AxisMarkSpan.Y) then
  begin
    PlotCanvas.MoveTo(Position.X - PlotCanvas.SmoothDistance(
      AxisMarkSpan.X / 2), Position.Y);
    PlotCanvas.LineTo(Position.X + PlotCanvas.SmoothDistance(
      AxisMarkSpan.X / 2), Position.Y);
  end
  else
  begin
    PlotCanvas.MoveTo(Position.X, Position.Y -
      PlotCanvas.SmoothDistance(AxisMarkSpan.Y / 2));
    PlotCanvas.LineTo(Position.X, Position.Y +
      PlotCanvas.SmoothDistance(AxisMarkSpan.Y / 2));
  end;
end;

procedure TCPGridLayer.DrawAxisTextMarks;
var
  a: Integer;
  PlotStyle: PCPPlotStyle;
  PlotCanvas: TCPPlotCanvas;
  AxisMarkSpan: TCPVector;
  MarkPos, P: TCPPoint;
  TextPadding, Coord: TCPFloat;
  B: TCPRect;
begin
  PlotStyle := FCoordSystem.FPlotStyle;
  PlotCanvas := FCoordSystem.FView.PlotCanvas;
  B := FCoordSystem.FView.ViewRect;
  // Calclulate position of vertical and horizontal description axis:
  MarkPos.X := EnsureRange(0, B.Left, B.Right);
  MarkPos.Y := EnsureRange(0, B.Top, B.Bottom);
  MarkPos := FCoordSystem.FView.ToCanvas(MarkPos);
  TextPadding := PlotCanvas.PtToUnits(PlotStyle.AxisTextPadding +
    PlotCanvas.PenWidth / 2);
  // Dimensions of axis mark
  AxisMarkSpan := CPToVector(
    PlotCanvas.PtToUnits(PlotStyle.GridLineWidth),
    PlotCanvas.PtToUnits(PlotStyle.AxisMarksLength));
  P := MarkPos;
  // Draw horizontal axis description:
  for a := -1 to FLineCount.X do
  begin
    P.X := GetVertGridLine(a);
    Coord := a * FGridCellSize.X + FTopLeft.X;
    if (Abs(Coord) > 1E-7) then
    begin
      DrawAxisMark(PlotCanvas, P, AxisMarkSpan);
    end;
    DrawAxisLabel(PlotCanvas, P, TextPadding, Coord, true);
  end;
  // Draw vertical axis description:
  CPSwap(AxisMarkSpan.X, AxisMarkSpan.Y);
  P.X := MarkPos.X;
  for a := -1 to FLineCount.Y do
  begin
    P.Y := GetHorizGridLine(a);
    Coord := a * FGridCellSize.Y + FTopLeft.Y;
    if (Abs(Coord) > 1E-7) then
    begin
      DrawAxisMark(PlotCanvas, P, AxisMarkSpan);
      DrawAxisLabel(PlotCanvas, P, TextPadding, Coord, false);
    end;
  end;
end;

function TCPGridLayer.GetHorizGridLine(Index: Integer): TCPFloat;
begin
  Result := FCoordSystem.FView.ToCanvas(CPToVector(
    0, FTopLeft.Y + Index * FGridCellSize.Y)).Y;
end;

function TCPGridLayer.GetVertGridLine(Index: Integer): TCPFloat;
begin
  Result := FCoordSystem.FView.ToCanvas(CPToVector(
    FTopLeft.X + Index * FGridCellSize.X, 0)).X;
end;

end.
